/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.interaction3d.assembly.convert.obj;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;


import java.net.URI;
import org.interaction3d.assembly.convert.Mesh;


import org.interaction3d.assembly.convert.MeshUtil;
import static java.lang.Double.parseDouble;
import static java.lang.Integer.parseInt;


/**
 * http://local.wasp.uwa.edu.au/~pbourke/dataformats/obj/
 * @author Michael Nischt
 */
public class ConverterOBJ
{
    private static final int POSITION = 0, TEXTURE = 1, NORMAL = 2;
    private static final String DEFAULT_MATERIAL = "";

    private final Map<String, Mesh> meshes = new HashMap<String, Mesh>();
    private List<String> mtllib = Collections.emptyList();

    private List<double[]> positionList, textureList, normalList;
    private Map<String, List<int[]>> triangleGroups;


    private int smoothingGroup = 0, totalFaces = 0;
    private String material = DEFAULT_MATERIAL;
    private String object   = "Default";
    private List<int[]> triangles;
          
    private boolean logPoints = true, logLines = true;

    public static void main(String... args) throws Exception
    {
        String desktop = System.getProperty("user.home") + File.separator + "Desktop" + File.separator;
        String target = "/home/micha/Projects/Cam3D/code/src/main/resources/de/tuberlin/qu/interaction3d/campnou/";

        String dst = target + "mesh";
        String src = desktop + "camp_nou_obj" + File.separator + "camp_nou.obj";

        convertFile(src, dst);
    }

    public static void convertFile(String src, String dst)
    {
        File dstFile = new File(dst);
        if(!dstFile.isDirectory() && !dstFile.mkdirs())
        {
            throw new IllegalArgumentException("Directory '" + dst + "' does not exist and cannot be created");
        }

        ConverterOBJ convert = new ConverterOBJ();
        convert.convert(new File(src));

        for(Map.Entry<String, Mesh> entry : convert.meshes.entrySet())
        {
            FileOutputStream out = null;
            try
            {
                out = new FileOutputStream(new File(dst + File.separator + entry.getKey() + ".mesh.xml"));
                entry.getValue().writeTo(out);
            }
            catch (IOException ex)
            {
                log(ex);
            }
            finally
            {
                try
                {
                    out.close();
                }
                catch (IOException ex)
                {
                    log(ex);
                }
            }
        }
    }

    // <editor-fold defaultstate="collapsed" desc="convert">

    public void convert(String uri)
    {
        try
        {
            InputStream in = URI.create(uri).toURL().openStream();
            try
            {
                convert(in);
            }
            finally
            {
                close(in);
            }
        }
        catch (IOException ex)
        {
            Logger.getLogger(ConverterMTL.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void convert(InputStream in)
    {
        try
        {
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            convert(reader);
        }
        catch (IOException ex)
        {
            log(ex);
            throw new RuntimeException(ex);
        }
    }

    public void convert(File f)
    {
        try
        {
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(f)));
            convertAndClose(reader);
        }
        catch (IOException e)
        {
            log(e);
            throw new RuntimeException(e);
        }
    }

    private void convertAndClose(BufferedReader reader) throws IOException
    {
        try
        {
            convert(reader);
        }
        finally
        {
            close(reader);
        }
    }

    static private void close(Closeable c)
    {
        try
        {
            c.close();
        }
        catch (IOException e)
        {
            log(e);
            throw new RuntimeException(e);
        }
    }

    // </editor-fold>

    private void convert(BufferedReader reader) throws IOException
    {
        positionList = new ArrayList<double[]>();        
        textureList  = new ArrayList<double[]>();
        normalList   = new ArrayList<double[]>();

        triangles = new ArrayList<int[]>();
        triangleGroups = new HashMap<String, List<int[]>>();
        triangleGroups.put(DEFAULT_MATERIAL, triangles);

        String line;

        while((line=reader.readLine()) != null)
        {
            line(line);
        }
        mesh(object);
    }


    // <editor-fold defaultstate="collapsed" desc="line">
    
    private void line(String line)
    {
        {   // get rid of comment
            int index = line.indexOf('#');
            if(index >= 0)
            {
                line = line.substring(0, index);
            }
        }
        line = line.trim();

        StringTokenizer scanner = new StringTokenizer(line);

        //empty line
        if(!scanner.hasMoreElements())
        {
            return;
        }

        String op = scanner.nextToken();


        if(op.equals("v"))
        {
            position(scanner);
        }
        else if(op.equals("vn"))
        {
            normal(scanner);
        }
        else if(op.equals("vt"))
        {
            texture(scanner);
        }
        else if(op.equals("p"))
        {
            points(scanner);
        }
        else if(op.equals("l"))
        {
            lines(scanner);
        }
        else if(op.equals("f"))
        {
            faces(scanner);
        }
        else if(op.equals("g"))
        {
            groups(scanner);
        }
        else if(op.equals("s"))
        {
            smoothingGroup(scanner.nextToken());
        }
        else if(op.equals("o"))
        {
            object(scanner.nextToken());
        }
        else if(op.equals("usemtl"))
        {
            usemtl(scanner.nextToken());
        }
        else if(op.equals("mtllib"))
        {
            mtllib(scanner);
        }
        else
        {
            log("Unknown: " + op);
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="vertices">

    private void position(StringTokenizer scanner)
    {
        // x y z (w=1)
        double x = parseDouble(scanner.nextToken());
        double y = parseDouble(scanner.nextToken());
        double z = parseDouble(scanner.nextToken());

        double[] position;
        if(scanner.hasMoreTokens())
        {
            position = new double[]
            {
                x, y, z, parseDouble(scanner.nextToken())
            };
        }
        else
        {
            position = new double[]
            {
                x, y, z
            };
        }
        positionList.add(position);
    }    

    private void texture(StringTokenizer scanner)
    {
        // u (v=0) (w=0)
        double u = parseDouble(scanner.nextToken());

        double[] texture;
        if(scanner.hasMoreTokens())
        {
            double v = parseDouble(scanner.nextToken());

            if(scanner.hasMoreTokens())
            {
                texture = new double[]
                {
                    u, v, parseDouble(scanner.nextToken())
                };
            }
            else
            {
                texture = new double[]
                {
                    u, v
                };
            }
        }
        else
        {
            texture = new double[]
            {
                u
            };
        }
        textureList.add(texture);
    }

    private void normal(StringTokenizer scanner)
    {
        // i j k
        double[] normal =
        {
            parseDouble(scanner.nextToken()),
            parseDouble(scanner.nextToken()),
            parseDouble(scanner.nextToken())
        };

        normalList.add(normal);
    }

    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="primitives">

    private void points(StringTokenizer scanner)
    {
        if (true)
        {
            if (logPoints)
            {
                logPoints = false;
                log("Lines (l) are not supported");
            }
            return;
        }

        // v1 v2 v3 . . .
        while (scanner.hasMoreTokens())
        {
            int p = parseInt(scanner.nextToken());

            p += (p < 0) ? positionList.size() : -1;
        }
    }

    private void lines(StringTokenizer scanner)
    {
        if(true)
        {
            if(logLines)
            {
                logLines = false;
                log("Lines (l) are not supported");
            }
            return;
        }

        // v1/vt1 v2/vt2 v3/vt3 . . .
        // can be v/t or v
        while(scanner.hasMoreTokens())
        {
            int p,t;
            {
                String vert = scanner.nextToken();
                int split = vert.indexOf('/');
                if(split < 0) split = vert.length();

                p = parseInt(vert.substring(0, split++));
                t = (split < vert.length()) ? t = parseInt(vert.substring(split)) : 0;
            }

            p += (p < 0) ?  positionList.size() : -1;
            t += (t < 0) ?  textureList.size() : -1;
        }

    }

    private void faces(StringTokenizer scanner)
    {
        totalFaces++;

        List<int[]> vertexList = new ArrayList<int[]>();
        // v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 . . .
        // can be v/t/n or v/t or v//n or v
        while(scanner.hasMoreTokens())
        {
            int p,t,n;
            {
                String vert = scanner.nextToken();
                int split1 = vert.indexOf('/');
                if(split1 < 0) split1 = vert.length();
                int split2 = vert.indexOf('/', split1+1);
                if(split2 < 0) split2 = vert.length();


                p = parseInt(vert.substring(0, split1++));
                t = (split1 < split2) ? parseInt(vert.substring(split1, split2)) : 0;
                split2++;
                n = (split2 < vert.length()) ?  parseInt(vert.substring(split2)) : 0;
            }

            p += (p < 0) ?  positionList.size() : -1;
            t += (t < 0) ?  textureList.size() : -1;
            n += (n < 0) ?  normalList.size() : -1;

            vertexList.add(new int[] { p, t, n } );
        }


        double[] faceNormal;
        int[] map;
        {
            int numVertices = vertexList.size();
            double[][] polygon = new double[numVertices][];
            for(int i=0; i<numVertices; i++)
            {
                polygon[i] = positionList.get(vertexList.get(i)[POSITION]);
            }
            faceNormal = faceNormal(polygon);
            map = triangulize(polygon);
        }

        boolean added = false;
        for(int[] vertex : vertexList)
        {
            if(vertex[NORMAL] >= 0)
            {
                continue;
            }
            else if(smoothingGroup > 0)
            {
                vertex[NORMAL] = -smoothingGroup;
            }
            else
            {
                if(!added)
                {
                    normalList.add(faceNormal);
                    added = true;
                }
                vertex[NORMAL] = normalList.size() - 1;
            }
        }

        for(int index : map)
        {
            triangles.add(vertexList.get(index));
        }

//        if(vertexList.size() == 3)
//        {
//            triangles.addAll(vertexList);
//        }
//        else
//        {
//            log("Only triangles are supported atm");
//        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="misc">

    private void mtllib(StringTokenizer scanner)
    {
        // filename1 filename2 . . .
        List<String> list = new ArrayList<String>();
        while(scanner.hasMoreTokens())
        {
            list.add(scanner.nextToken());
        }
        this.mtllib = list;
    }

    private void usemtl(String material)
    {
        if(triangleGroups.get(this.material).isEmpty())
        {
            triangleGroups.remove(this.material);
        }

        // material_name
        this.material = (material.equalsIgnoreCase("off"))
                      ? DEFAULT_MATERIAL
                      : material;

        triangles = triangleGroups.get(this.material);
        if(triangles == null)
        {
            triangles = new ArrayList<int[]>();
            triangleGroups.put(this.material, triangles);
        }
    }

    private void smoothingGroup(String group)
    {
        // group_number or 0 or 'off'
        smoothingGroup = (group.equalsIgnoreCase("off"))
                       ? 0
                       : parseInt(group);
    }

    private void object(String group)
    {
        // object_name
        mesh(object);
        object = group;
    }

    private void groups(StringTokenizer scanner)
    {
        // (group_name1=default) (group_name2) . . .
        log("Groups (g) are not supported");
    }

    // </editor-fold>

    void mesh(String name)
    {        
        if(positionList.isEmpty() || totalFaces == 0) return;

        totalFaces = 0;

        double[][] positions = positionList.toArray(new double[positionList.size()][]);
        positionList.clear();
        double[][] textures = textureList.toArray(new double[textureList.size()][]);
        textureList.clear();
        double[][] normals = normalList.toArray(new double[normalList.size()][]);
        normalList.clear();
        
        MeshUtil meshUtil = new MeshUtil(positions, textures, normals);
        for(Map.Entry<String,List<int[]>> entry : triangleGroups.entrySet())
        {
            String mat = entry.getKey();
            List<int[]> indices = entry.getValue();

            int numTriangles = indices.size() / 3;
            int[][] triPositions = new int[numTriangles][3];
            int[][] triTextures = new int[numTriangles][3];
            int[][] triNormals = new int[numTriangles][3];

            for(int i=0; i<numTriangles; i++)
            {
                int off = 3*i;
                for(int j=0; j<3; j++)
                {
                    triPositions[i][j] = indices.get(off+j)[POSITION];
                    triTextures[i][j]  = indices.get(off+j)[TEXTURE];
                    triNormals[i][j]   = indices.get(off+j)[NORMAL];
                }
            }

            meshUtil.primtives(mat, "Triangles", triPositions, triTextures, triNormals);
        }

        Mesh mesh = meshUtil.create();
        meshes.put(name, mesh);

        // <editor-fold defaultstate="collapsed" desc="old - keep until TESTED">
        ////weld
        //{
        //    Weld weldedPositions = new Weld();
        //    Weld weldedTextures = new Weld(1e-04);
        //    Weld weldedNormals = new Weld(1e-04);
        //
        //    for (List<int[]> indices : triangleGroups.values())
        //    {
        //        for (int[] index : indices)
        //        {
        //            int i;
        //            i = POSITION;
        //            if (index[i] >= 0)
        //            {
        //                index[i] = weldedPositions.apply(positions[index[i]]);
        //            }
        //            i = TEXTURE;
        //            if (index[i] >= 0)
        //            {
        //                index[i] = weldedTextures.apply(textures[index[i]]);
        //            }
        //            i = NORMAL;
        //            if (index[i] >= 0)
        //            {
        //                index[i] = weldedNormals.apply(normals[index[i]]);
        //            }
        //        }
        //    }
        //
        //    positions = weldedPositions.coordinates();
        //    textures = weldedTextures.coordinates();
        //    normals = weldedNormals.coordinates();
        //}
        //
        //boolean hasTexture = false;
        //Smooth smooth = new Smooth(positions.length);
        //{
        //    for (List<int[]> indices : triangleGroups.values())
        //    {
        //        int numIndices = indices.size();
        //        for (int i = 0; i < numIndices; i++)
        //        {
        //            int[] index = indices.get(i);
        //            if (index[TEXTURE] >= 0)
        //            {
        //                hasTexture = true;
        //            }
        //
        //            index = new int[]
        //                {
        //                    smooth.apply(index[POSITION], index[NORMAL]),
        //                    index[TEXTURE]
        //                };
        //            indices.set(i, index);
        //        }
        //    }
        //    if (!hasTexture)
        //        for (List<int[]> indices : triangleGroups.values())
        //        {
        //            int numIndices = indices.size();
        //            for (int i = 0; i < numIndices; i++)
        //            {
        //                int[] index = indices.get(i);
        //                index = new int[]
        //                    {
        //                        index[POSITION]
        //                    };
        //                indices.set(i, index);
        //            }
        //        }
        //}
        //
        //Vertices vertices = new Vertices();
        //Mesh.PrimitiveGroup[] primitiveGroups = new Mesh.PrimitiveGroup[triangleGroups.size()];
        //{
        //    int index = 0;
        //    for (Map.Entry<String, List<int[]>> entry : triangleGroups.entrySet())
        //    {
        //        String groupMaterial = entry.getKey();
        //        List<int[]> indices = entry.getValue();
        //
        //        int numTriangles = indices.size() / 3;
        //        int[][] primitives = new int[numTriangles][3];
        //        for (int i = 0; i < numTriangles; i++)
        //        {
        //            int j = 3 * i;
        //            primitives[i][0] = vertices.apply(indices.get(j + 0));
        //            primitives[i][1] = vertices.apply(indices.get(j + 1));
        //            primitives[i][2] = vertices.apply(indices.get(j + 2));
        //        }
        //        primitiveGroups[index++] = new Mesh.PrimitiveGroup(groupMaterial, "Triangles", primitives);
        //    }
        //}
        //
        //int[][] maps = vertices.attributes();
        //int[] remap = Vertices.sort(maps, POSITION);
        //for (Mesh.PrimitiveGroup primitiveGroup : primitiveGroups)
        //{
        //    for (int[] p : primitiveGroup.primitives)
        //    {
        //        for (int i = 0; i < p.length; i++)
        //        {
        //            p[i] = remap[p[i]];
        //        }
        //    }
        //}
        //
        //maps = Vertices.transpose(maps);
        //int[] mapIndices = new int[maps.length];
        //maps = Vertices.reduce(maps, mapIndices);
        //
        //Mesh.VertexElement[] elements = new Mesh.VertexElement[mapIndices.length];
        //elements[POSITION] = new Mesh.VertexElement("POSITION", positions, smooth.split(), mapIndices[POSITION]);
        //if (elements.length > 1)
        //{
        //    elements[TEXTURE] = new Mesh.VertexElement("TEXTURE", textures, mapIndices[TEXTURE]);
        //}
        //
        //
        //Mesh mesh = new Mesh(elements, maps, primitiveGroups);
        //meshes.put(name, mesh);
        // </editor-fold>
    }

    protected double[] faceNormal(double[][] polygon)
    {
        double[] ab =
        {
            polygon[1][0] - polygon[0][0],
            polygon[1][1] - polygon[0][1],
            polygon[1][2] - polygon[0][2]
        };

        double[] ac =
        {
            polygon[2][0] - polygon[0][0],
            polygon[2][1] - polygon[0][1],
            polygon[2][2] - polygon[0][2]
        };

        double[] cross =
        {
            -ab[2]*ac[1] + ab[1]*ac[2],
            ab[2]*ac[0] - ab[0]*ac[2],
            -ab[1]*ac[0] + ab[0]*ac[1]
        };

        double norm = Math.sqrt(cross[0]*cross[0] + cross[1]*cross[1] + cross[2]*cross[2]);

        cross[0] /= norm;
        cross[1] /= norm;
        cross[2] /= norm;

        return cross;
    }

    protected int[] triangulize(double[][] polygon)
    {
        int numTriangles = polygon.length-2;
        int[] map = new int[3*numTriangles];

        for(int i=0; i<numTriangles; i++)
        {
            map[3*i+0] = 0;
            map[3*i+1] = i+1;
            map[3*i+2] = i+2;
        }

        return map;
    }

    // <editor-fold defaultstate="collapsed" desc="log">

    static private void log(String msg)
    {
        Logger.getLogger(ConverterOBJ.class.getName()).log(Level.WARNING, msg);
    }

    static private void log(Throwable thrown)
    {
        log(null, thrown);
    }

    static private void log(String msg, Throwable thrown)
    {
        Logger.getLogger(ConverterOBJ.class.getName()).log(Level.SEVERE, msg, thrown);
    }

    // </editor-fold>
}